package roborallyimplementation;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import be.kuleuven.cs.som.annotate.*;

/**
 * A class of boards representative for the game board used for RoboRally.
 * 
 * @author Thomas Vochten (1ba inform.), Daan Daele (1ba inform.)
 * 
 * @invar	Each position that is in use on this board is a valid position.
 * 			|	for each position in this.getPositionsInUse(): isValidPosition(position)
 * @invar	This board can have its width as width.
 * 			|	this.canHaveAsWidth(this.getWidth())
 * @invar	This board can have its height as height.
 * 			|	this.canHaveAsHeight(this.getHeight())
 *
 */
public class Board {

	/**
	 * Returns a new board with the given width as width and the given height as height.
	 * 
	 * @param	width
	 * 			The width that the new board shall have.
	 * @param	height
	 * 			The height that the new board shall have.
	 * @post	The width and height of the new board equal the given width and the given height.
	 * @throws	IllegalArgumentException
	 * 			The new board cannot have the given width as width or cannot have the given height as height.
	 * 			|	! new.canHaveAsWidth(width) || ! new.canHaveAsHeight(height)
	 */
	@Raw
	public Board(long width, long height) throws IllegalArgumentException
	{
		if (! canHaveAsWidth(width) || ! canHaveAsHeight(height))
			throw new IllegalArgumentException();
		this.width = width;
		this.height = height;
	}
	
	/**
	 * Returns the upper bound on widths associated with all boards
	 */
	@Basic
	public static long getMaxWidth()
	{
		return Long.MAX_VALUE;
	}
	
	/**
	 * Returns the upper bound on heights associated with all boards.
	 */
	public static long getMaxHeight()
	{
		return Long.MAX_VALUE;
	}
	
	/**
	 * Calculates whether this board can have the given width as width.
	 * 
	 * @param	width
	 * 			The width to be considered.
	 * @return	True if and only if the given width does not exceed the upper bound on widths
	 * 			and if the given width is a positive number, false otherwise.
	 * 			|	result == width <= getMaxWidth() && width >= 0
	 */
	@Raw
	public boolean canHaveAsWidth(long width)
	{
		return width <= getMaxWidth() && width >= 0;
	}
	
	/**
	 * Calculates whether this board can have the given height as height.
	 * 
	 * @param	height
	 * 			The height to be considered.
	 * @return	True if and only if the given height does not exceed the upper bound on heights
	 * 			and if the given height is a positive number, false otherwise.
	 * 			|	result == height <= getMaxHeight() && height >= 0
	 */
	@Raw
	public boolean canHaveAsHeight(long height)
	{
		return height <= getMaxHeight() && height >= 0;
	}
	
	/**
	 * Get the width of this board.
	 */
	public long getWidth()
	{
		return width;
	}
	
	/**
	 * Get the height of this board.
	 */
	public long getHeight()
	{
		return height;
	}
	
	/**
	 * Calculates whether this position can exist on this board.
	 * 
	 * @param	position
	 * 			The position to be considered.
	 * @return	True if and only if the x-coordinate does not exceed the upper bound on widths
	 * 			and if it is a positive number (and likewise for the y-coordinate, except that it must not
	 * 			exceed the upper bound on heights), false otherwise. Furthermore, if this board has been
	 * 			terminated, the result will always be false.
	 * 			|	if (this.isTerminated()) then result == false
	 * 			|	result == position != null (position.getX() <= this.getMaxWidth() && position.getX() >= 0 && position.getY()
	 * 			|	<= this.getMaxHeight() && position.getY() >= 0)
	 */
	public boolean isValidPosition(Position position)
	{
		if (this.isTerminated())
			return false;
		return position != null && (position.getX() <= getWidth() && position.getX() >= 0 && 
				position.getY() <= getHeight() && position.getY() >= 0);
	}
	
	private long width;
	private long height;
	
	/**
	 * Returns all elements that are located on the given position.
	 * 
	 * @param	position
	 * 			The positions whose elements will be returned.
	 * @return	This method calls the getElements() position in the Position class. However, if this
	 * 			board has been terminated or if this board has not taken the given position in use,
	 * 			this method will always return a null pointer
	 * 			|	if (this.isTerminated || ! this.getPositionsInUse().containsKey(position.toCoordinatePair()))
	 * 			|	then result == null
	 * 			|	result == position.getElements()
	 * @throws	IllegalPositionException
	 * 			The given position is beyond the bounds of this board.
	 * 			|	! isValidPosition(position)
	 */
	public List<Element> getElements(Position position) throws IllegalPositionException
	{
		if (this.isTerminated())
			return null;
		if (! isValidPosition(position))
			throw new IllegalPositionException("Cannot access a position beyond the bounds of a board.",
					position.getX(), position.getY());
		if (this.getPositionsInUse().containsKey(position.toCoordinatePair()))
			return this.getPositionsInUse().get(position.toCoordinatePair()).getElements();
		return null;
	}
	
	/**
	 * Returns all elements that are located on this board.
	 * 
	 * @return	All elements located on this board.
	 */
	
	@Basic
	public Set<Element> getElements()
	{
		return elementsOnBoard;
	}
	
	/**
	 * Returns all elements of this board that are of the given class.
	 * 
	 * @param	c
	 * 			The subclass of Element of which all participating objects on this board will be returned.
	 * @return	All elements on this board that belong to the given class. However, is this board has
	 * 			been terminated, this method will always return a null pointer.
	 * 			|	if (this.isTerminated()) then result == null
	 * 			|	for each element in this.getElements():
	 * 			|		if (c.isInstance(element)) then result.contains(element)
	 * @throws	IllegalArgumentException
	 * 			c is equal to the Element class.
	 * 			|	c == Element.class
	 */
	@SuppressWarnings("unchecked")
	public <T extends Element> Set<T> getAllElementsOfType(Class<T> c) throws IllegalArgumentException
	{
		if (this.isTerminated())
			return null;
		if (c == Element.class)
			throw new IllegalArgumentException();
		Set<T> toreturn = new HashSet<T>();
		for (Element element : this.getElements())
		{
			if (c.isInstance(element))
				toreturn.add((T) element);
		}
		return toreturn;
			
	}
	
	/**
	 * Determines whether the given element can exist on this board.
	 * 
	 * @param	element
	 * 			The element to be considered.
	 * @return	True if and only if the position of the given element is a valid position for this board
	 * 			and if that	position can have the given element added to it.
	 * 			Furthermore, if this board has been terminated, this method will always return false.
	 * 			|	if (this.isTerminated()) then result == false
	 * 			|	result == (this.isValidPosition(element.getPösition()) && 
	 * 			|				this.getPositionsInUse().containsKey(element.getPosition().toCoordinatePair())
	 * 			|				this.getPositionsInUse().get(element.getPosition().toCoordinatePair()).canAddThisElement(element))
	 */
	public boolean canExistOnThisBoard(Element element)
	{
		if (this.isTerminated() || element.isTerminated())
			return false;
		if (! this.isValidPosition(element.getPosition()))
			return false;
		if (! this.getPositionsInUse().containsKey(element.getPosition().toCoordinatePair()))
			return true;
		return this.getPositionsInUse().get(element.getPosition().toCoordinatePair()).canAddThisElement(element);
	}
	
	/**
	 * Places the given element on the given position.
	 * 
	 * @param	position
	 * 			The position on which the element is to be placed.
	 * @param	element
	 * 			The element to be placed.
	 * @post	The given element is added to the given position
	 * 			|	(new position).contains(element)
	 * @throws	IllegalPositionException
	 * 			The given position is beyond the bounds of this board.
	 * 			|	! isValidPosition(position)
	 * @throws	IllegalArgumentException
	 * 			The given element could not be added to the given position or this board already contains the given element.
	 * 			|	! element.canHaveAsBoard(this) || ! position.canAddThisElement(element) || this.getElements().contains(element)
	 */
	public void putElement(Position position, Element element) throws IllegalPositionException, IllegalArgumentException
	{
		CoordinatePair pair = position.toCoordinatePair();
		if (! isValidPosition(position))
			throw new IllegalPositionException("Cannot access a position beyond the bounds of a board.",
					position.getX(), position.getY());
		if (! element.canHaveAsBoard(this))
			throw new IllegalArgumentException();
		if (this.getElements().contains(element))
			throw new IllegalArgumentException();
		if (this.getPositionsInUse().containsKey(pair) &&
				! this.getPositionsInUse().get(pair).canAddThisElement(element))
			throw new IllegalArgumentException();
		
		element.setBoard(this);
		element.setPosition(position);
		this.addPosition(position);
		this.getPositionsInUse().get(pair).placeElement(element);
		this.getElements().add(element);
	}
	
	/**
	 * Remove the given element from this board.
	 * 
	 * @param	element
	 * 			The element to be removed.
	 * @post	If this board contained this element, it will have been removed.
	 * 			|	if (this.contains(element)) then ! new.contains(element)
	 */
	public void removeElement(Element element)
	{
		if (this.isTerminated())
			return;
		if (! this.getElements().contains(element))
			return;
		CoordinatePair pair = element.getPosition().toCoordinatePair();
		this.getPositionsInUse().get(pair).removeElement(element);
		if (this.getPositionsInUse().get(pair).getElements().isEmpty())
			this.getPositionsInUse().remove(pair);
		this.getElements().remove(element);
		element.setBoard(null);
		element.setPosition(null);
	}
	
	private Set<Element> elementsOnBoard = new HashSet<Element>();
	
	/**
	 * Gets the map of positions that are currently in use.
	 */
	@Basic
	public Map<CoordinatePair, Position> getPositionsInUse()
	{
		return positionsInUse;
	}
	
	/**
	 * Returns the position in this.getPositionsInUse() that is equal to the given position, if any.
	 * 
	 * @param	position
	 * 			The requested position.
	 * @return	If the coordinate pair associated with the given position is among the keys in 
	 * 			this.getPositionsInUse(), returns the position that is equal to the given position.
	 * 			|	if (this.getPositionsInUse().containsKey(position.toCoordinatePair())
	 * 			|		result == this.getPositionsInUse().get(position.toCoordinatePair())
	 * @throws	NullPointerException
	 * 			The given position is non-effective.
	 * 			|	position == null
	 */
	public Position getPosition(Position position) throws NullPointerException
	{
		if (position == null)
			throw new NullPointerException();
		CoordinatePair pair = position.toCoordinatePair();
		if (this.getPositionsInUse().containsKey(pair))
			return this.getPositionsInUse().get(pair);
		return null;
	}
	
	/**
	 * Add the given position to the set of positions that are currently in use.
	 * 
	 * @param	position
	 * 			The position to be added.
	 * @post	The given position will have been added to the set of positions in use if
	 * 			it was not previously in use.
	 * 			|	if (! this.getPositionsInUse().contains(position) then
	 * 				new.getPositionsInUse().contains(position)
	 * @throws	IllegalPositionException
	 * 			The given position was beyond the bounds of this board
	 * 			|	! isValidPosition(position)
	 */
	@Model
	private void addPosition(Position position) throws IllegalPositionException
	{
		if (this.isTerminated())
			return;
		if (! isValidPosition(position))
		{
			if (position == null)
				throw new IllegalPositionException("Cannot take non-effective positions in use.");
			else throw new IllegalPositionException("Cannot add positions beyond the bounds of a board",
					position.getX(), position.getY());
		}
		if (! this.getPositionsInUse().containsKey(position.toCoordinatePair()))
			this.getPositionsInUse().put(position.toCoordinatePair(), position);
	}
	
	private Map<CoordinatePair, Position> positionsInUse = new HashMap<CoordinatePair, Position>();
	
	/**
	 * Merges this board together with the other board as much as possible.
	 * 
	 * @param	other
	 * 			The board that will be merged with this board.
	 * @post	All elements that belong to the second board will have been moved if those elements
	 * 			are on a position that can exist on this board and if those elements can be added
	 * 			to that position (that is, the version of the position that is in use on this board).
	 * 			|	for each element in other.getElements():
	 * 			|		if (this.isValidPosition(element.getPosition() && 
	 * 			|		this.getPositionsInUse().get(element.getPosition().toCoordinatePair()).canAddThisElement(element))
	 * 			|			new.getElements().contains(element)
	 * @post	All elements that cannot exist on this board will have been terminated.
	 * 			|	for each element in other.getElements():
	 * 			|		if (! (this.isValidPosition(element.getPosition() || 
	 * 			|		this.getPositionsInUse().get(element.getPosition().toCoordinatePair()).canAddThisElement(element)))
	 * 			|			(new element).isTerminated() == true
	 */
	public void merge(Board other)
	{
		if (this.isTerminated() || other.isTerminated())
			return;
		for (Object obj : other.getElements().toArray())
		{
			Element element = (Element) obj;
			Position pos = element.getPosition(); 
			if (this.isValidPosition(pos))
			{
				if (this.getPositionsInUse().containsKey(pos.toCoordinatePair()) &&
					this.getPositionsInUse().get(pos.toCoordinatePair()).canAddThisElement(element))
				{
				other.removeElement(element);
				this.putElement(pos, element);
				}
				else if (! this.getPositionsInUse().containsKey(pos.toCoordinatePair()))
				{
					other.removeElement(element);
					this.putElement(pos, element);
				}
			}
		}
		other.terminate();
	}
	
	/**
	 * Terminates this board, along with all elements located on this board.
	 * 
	 *  @post	This board will have been terminated.
	 *  		|	new.isTerminated() == true
	 *  @post	All elements on this board will have been terminated.
	 *  		|	for each element in this.getElements(): (new element).isTerminated() == true
	 * 
	 */
	public void terminate()
	{
		if (! this.isTerminated())
		{
			Object[] elementarray = this.getElements().toArray();
			
			for (Object element: elementarray)
				 ((Element) element).terminate();
			
			positionsInUse = null;
			elementsOnBoard = null;
			isTerminated = true;
		}
	}
	
	/**
	 * Returns whether this board has been terminated.
	 */
	@Basic
	public boolean isTerminated()
	{
		return isTerminated;
	}
	
	private boolean isTerminated = false;
}
